import { getImage } from "../../core/ajax";
import { Texture2D } from "../../texture/Texture2D";
import { TextureCube } from "../../texture/TextureCube";
import { Texture2DArray } from "../../texture/Texture2DArray";
import { TextureGPU } from "../../texture/TextureGPU";
import { TextureCompressedArrayBuffer } from "../../texture/TextureCompressedArrayBuffer";
import { guid, isBase64 } from "../../core/utils";
import { Vector3 } from "../../core/math/Vector3";
import { Matrix4 } from "../../core/math/Matrix4";
import { Engine } from "../../Engine";
import { SkyboxIndices, SkyboxPosition } from "../../geom/SkyboxGeometry";
import {
  generate_skybox_vertex_shader,
  skybox_fragment_shader,
  captureDirections,
  generateIBL,
} from "./SkyBoxShader";
import { TextureData } from "../../texture/TextureData";
/**
 * @memberof w.loader
 */
class TextureLoader {
  static instance = null;
  constructor() {
    if (TextureLoader.instance) {
      return TextureLoader.instance;
    }
    this.textureMap = new Map();
    this.textureRequestMap = new Map();
    TextureLoader.instance = this;
  }
  async loadTexture2D(url, opts = {}) {
    let { id } = opts;
    if (!this.textureMap.has(id)) {
      const texture = new Texture2D(opts);
      if (url.constructor.name === "String") {
        let { image } = await this.requestImage(url, opts);
        texture.image = image;
      } else if (url.constructor.name === "ImageBitmap") {
        texture.image = url;
      }
      let gpuTexture = await texture.toGPUAsync();
      gpuTexture.id=id
      if (opts.isCache) {
        this.textureMap.set(id, gpuTexture);
      }
      return gpuTexture
    } else {
      return this.textureMap.get(id);
    }
  }
  async loadTextureHDR(url, opts = { exposure: 1, gamma: 2.2 }) {
    let { exposure, gamma, id, device, format, context } = opts;
    if (!device) device = Engine.instance.device;
    if (!format) format = Engine.instance.format;
    if (!context) context = Engine.instance.context;
    const response = await fetch(url);
    if (!response.ok) {
      const message = `An error has occured: ${response.status}`;
      throw new Error(message);
    }
    const result = await response.arrayBuffer();
    var header = "",
      pos = 0,
      d8 = new Uint8Array(result),
      formatHdr;
    // 读取头部.
    while (!header.match(/\n\n[^\n]+\n/g)) header += String.fromCharCode(d8[pos++]);
    // 格式.
    formatHdr = header.match(/FORMAT=(.*)$/m)[1];
    if (formatHdr != "32-bit_rle_rgbe")
      return console.warn("unknown format : " + formatHdr), this.onerror();
    // 分辨率
    var rez = header.split(/\n/).reverse()[1].split(" "),
      width = rez[3] * 1,
      height = rez[1] * 1;
    // 分辨率
    var img = new Uint8Array(width * height * 4),
      ipos = 0;
    // Read all scanlines
    for (var j = 0; j < height; j++) {
      var rgbe = d8.slice(pos, (pos += 4)),
        scanline = [];
      if (rgbe[0] != 2 || rgbe[1] != 2 || rgbe[2] & 0x80) {
        var len = width,
          rs = 0;
        pos -= 4;
        while (len > 0) {
          img.set(d8.slice(pos, (pos += 4)), ipos);
          if (img[ipos] == 1 && img[ipos + 1] == 1 && img[ipos + 2] == 1) {
            for (img[ipos + 3] << rs; i > 0; i--) {
              img.set(img.slice(ipos - 4, ipos), ipos);
              ipos += 4;
              len--;
            }
            rs += 8;
          } else {
            len--;
            ipos += 4;
            rs = 0;
          }
        }
      } else {
        if ((rgbe[2] << 8) + rgbe[3] != width)
          return console.warn("HDR line mismatch .."), this.onerror();
        for (var i = 0; i < 4; i++) {
          var ptr = i * width,
            ptr_end = (i + 1) * width,
            buf,
            count;
          while (ptr < ptr_end) {
            buf = d8.slice(pos, (pos += 2));
            if (buf[0] > 128) {
              count = buf[0] - 128;
              while (count-- > 0) scanline[ptr++] = buf[1];
            } else {
              count = buf[0] - 1;
              scanline[ptr++] = buf[1];
              while (count-- > 0) scanline[ptr++] = d8[pos++];
            }
          }
        }
        for (var i = 0; i < width; i++) {
          img[ipos++] = scanline[i];
          img[ipos++] = scanline[i + width];
          img[ipos++] = scanline[i + 2 * width];
          img[ipos++] = scanline[i + 3 * width];
        }
      }
    }
    var canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    var context2d = canvas.getContext("2d");
    var HDRD = context2d.getImageData(0, 0, width, height);
    var res = HDRD.data;
    exposure = Math.pow(2, exposure === undefined ? 1 : exposure) / 2;
    if (gamma === undefined) gamma = 2.2;
    var one_over_gamma = 1 / gamma,
      s,
      l = img.byteLength >> 2,
      res = res || new Uint8ClampedArray(l * 4);
    for (var i = 0; i < l; i++) {
      s = exposure * Math.pow(2, img[i * 4 + 3] - (128 + 8));
      res[i * 4] = 255 * Math.pow(img[i * 4] * s, one_over_gamma);
      res[i * 4 + 1] = 255 * Math.pow(img[i * 4 + 1] * s, one_over_gamma);
      res[i * 4 + 2] = 255 * Math.pow(img[i * 4 + 2] * s, one_over_gamma);
      res[i * 4 + 3] = 255;
    }
    // context2d.putImageData(HDRD, 0, 0)
    // document.body.append(canvas)
    let rgbaData = new Uint8Array(res);
    const gpuTexture = device.createTexture({
      label: "yellow F on red",
      size: [width, height],
      format: "rgba8unorm",
      usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
    });
    device.queue.writeTexture(
      { texture: gpuTexture },
      rgbaData,
      { bytesPerRow: width * 4 },
      { width: width, height: height }
    );
    // 转cubeMap
    const positions = SkyboxPosition;
    const indices = SkyboxIndices;
    // 创建顶点缓冲区
    const positionBuffer = device.createBuffer({
      size: positions.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    const indicesBuffer = device.createBuffer({
      size: indices.byteLength,
      usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(positionBuffer, 0, positions);
    device.queue.writeBuffer(indicesBuffer, 0, indices);

    let directions = [];
    captureDirections.map(item => {
      const m = new Matrix4();
      m.lookAt(new Vector3(...item[0]), new Vector3(...item[1]), new Vector3(...item[2]));
      directions.push(new Float32Array(m.elements));
    });
    // 创建一个4x4的MVP矩阵
    const uPMatrix = device.createBuffer({
      label: "uPMatrix",
      size: 4 * 4 * 4, // 4 x 4 x float32
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
    const pM = new Float32Array([
      1.0000000000000002, 0, 0, 0, 0, 1.0000000000000002, 0, 0, 0, 0, -1.1008403361344539,
      -1.0084033613445378, 0, 0, -1, 0,
    ]);
    device.queue.writeBuffer(uPMatrix, 0, pM.buffer, pM.byteOffset, pM.byteLength);
    const viewMatrix = device.createBuffer({
      label: "vMatrix",
      size: 4 * 4 * 4, // 4 x 4 x float32
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
    // 为Matrix2创建一个统一的组
    const uniformBufferSize = 4 * 4 * 4; // 4x4 matrix
    const sampler = device.createSampler({
      magFilter: "linear",
      minFilter: "linear",
    });
    const isHdrBuffer = device.createBuffer({
      label: "isHdrBuffer",
      size: 4,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
    const isHdrData = new Uint32Array([1]);
    device.queue.writeBuffer(isHdrBuffer, 0, isHdrData);
    const pipelineCubeMap = await device.createRenderPipelineAsync({
      label: "pipelineCubeMap",
      layout: "auto",
      vertex: {
        module: device.createShaderModule({
          code: generate_skybox_vertex_shader(),
        }),
        entryPoint: "main",
        buffers: [
          {
            arrayStride: 3 * 4,
            attributes: [
              {
                shaderLocation: 0,
                format: "float32x3",
                offset: 0,
              },
            ],
          },
        ],
      },
      fragment: {
        module: device.createShaderModule({
          code: skybox_fragment_shader(),
        }),
        entryPoint: "main",
        targets: [
          {
            format: format,
          },
        ],
      },
      primitive: {
        topology: "triangle-list",
        cullMode: "none",
      },
      depthStencil: {
        depthWriteEnabled: true,
        depthCompare: "less",
        format: "depth24plus",
      },
      // multisample: {
      //   count: 4,
      // }
    });
    const vertexBindGroupGenCubeMap = device.createBindGroup({
      layout: pipelineCubeMap.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uPMatrix,
            offset: 0,
            size: uniformBufferSize,
          },
        },
        {
          binding: 1,
          resource: {
            buffer: viewMatrix,
            offset: 0,
            size: uniformBufferSize,
          },
        },
      ],
    });
    const fragmentBindGroupGenCubeMap = device.createBindGroup({
      layout: pipelineCubeMap.getBindGroupLayout(1),
      entries: [
        {
          binding: 0,
          resource: sampler,
        },
        {
          binding: 1,
          resource: gpuTexture.createView({
            dimension: "2d",
            //  format: "rgba16float"
          }),
        },
        {
          binding: 2,
          resource: {
            buffer: isHdrBuffer,
            offset: 0,
            size: 4,
          },
        },
      ],
    });
    const cubeMap = device.createTexture({
      dimension: "2d",
      size: [1024, 1024, 6],
      format: format,
      // mipLevelCount: 1,
      usage:
        GPUTextureUsage.TEXTURE_BINDING |
        GPUTextureUsage.COPY_DST |
        GPUTextureUsage.COPY_SRC |
        GPUTextureUsage.RENDER_ATTACHMENT,
    });
    // 为renderPass创建depthTexture
    const depthGenCubeMap = device.createTexture({
      size: [1024, 1024],
      format: "depth24plus",
      // sampleCount: 4,
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });
    const renderGenCubeMap = device.createTexture({
      size: [1024, 1024],
      format: format,
      // sampleCount: 4,
      usage:
        GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
    });
    const renderPassDescriptorGenCubeMap = {
      colorAttachments: [
        {
          view: renderGenCubeMap.createView(),
          clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
          loadOp: "clear",
          storeOp: "store",
        },
      ],
      depthStencilAttachment: {
        view: depthGenCubeMap.createView(),
        depthClearValue: 1.0,
        depthLoadOp: "clear",
        depthStoreOp: "store",
      },
    };
    for (var i = 0; i < 6; ++i) {
      const commandEncoder = device.createCommandEncoder();
      const passEncoderGenCubeMap = commandEncoder.beginRenderPass(renderPassDescriptorGenCubeMap);
      passEncoderGenCubeMap.setPipeline(pipelineCubeMap);

      device.queue.writeBuffer(
        viewMatrix,
        0,
        directions[i].buffer,
        directions[i].byteOffset,
        directions[i].byteLength
      );
      passEncoderGenCubeMap.setVertexBuffer(0, positionBuffer);
      passEncoderGenCubeMap.setIndexBuffer(indicesBuffer, "uint16");
      passEncoderGenCubeMap.setBindGroup(0, vertexBindGroupGenCubeMap);
      passEncoderGenCubeMap.setBindGroup(1, fragmentBindGroupGenCubeMap);
      passEncoderGenCubeMap.drawIndexed(indices.length, 1, 0, 0);
      passEncoderGenCubeMap.end();
      commandEncoder.copyTextureToTexture(
        {
          texture: renderGenCubeMap,
        },
        {
          texture: cubeMap,
          origin: [0, 0, i],
        },
        [1024, 1024]
      );
      device.queue.submit([commandEncoder.finish()]);
    }
    const textureGPU = new TextureGPU({ image: cubeMap, id: id, format: format });
    this.textureMap.set(id, textureGPU);
    if (opts.ibl) {
      const { cubeMapIrradiance, cubeMapPrefilter, renderTextureLut } = await generateIBL(
        cubeMap,
        device,
        format,
        context
      );
      // const textureGPU1 = new TextureGPU({ image: cubeMapIrradiance, id: id, format: format });
      // this.textureMap.set(id, textureGPU1);
      return {
        cubeMap: textureGPU,
        ibl: {
          irradianceMap: cubeMapIrradiance,
          prefilterMap: cubeMapPrefilter,
          brdfLUT: renderTextureLut,
        },
      };
    } else {
      return textureGPU;
    }
  }
  async loadTextureCube(urls, opts = {}) {
    let { id, device, format, context } = opts;
    if (!device) device = Engine.instance.device;
    if (!format) format = Engine.instance.format;
    if (!context) context = Engine.instance.context;
    if (!id) id = guid();
    let texture = null;
    if (!this.textureMap.has(id)) {
      texture = new TextureCube(opts);
      let images = [];
      if (isBase64(urls[0])) {//如果是base64需要单独处理
        for (let index = 0; index < urls.length; index++) {
          const url = urls[index];
          var img = new Image();
          img.src = url;
          await img.decode();
          let imageBitmap = await createImageBitmap(img);
          images.push(imageBitmap);
        }
      } else {
        let promises = urls
          .filter(url => !this.textureMap.has(url))
          .filter(url => url.constructor.name === "String")
          .map(item => this.requestImage(item));
        let requestData = await Promise.all(promises);
        urls.forEach(async url => {
          if (url.constructor.name !== "String") {
            images.push(url);
          } else if (this.textureMap.has(url)) {
            images.push(this.textureMap.get(url).image);
          } else {
            images.push(requestData.find(item => item.url === url).image);
          }
        });
      }

      texture.image = images;
      this.textureMap.set(id, texture);
    }
    if (opts.ibl) {
      const texture = this.textureMap.get(id);
      const imageBitMap = texture.image;
      const cubeMapTexture = device.createTexture({
        dimension: "2d",
        size: [imageBitMap[0].width, imageBitMap[0].height, 6],
        format: format,
        usage:
          GPUTextureUsage.TEXTURE_BINDING |
          GPUTextureUsage.COPY_DST |
          GPUTextureUsage.RENDER_ATTACHMENT,
      });
      for (let index = 0; index < 6; index++) {
        const bitMap = imageBitMap[index];
        device.queue.copyExternalImageToTexture(
          { source: bitMap },
          { texture: cubeMapTexture, origin: [0, 0, index] },
          [bitMap.width, bitMap.height]
        );
      }

      const { cubeMapIrradiance, cubeMapPrefilter, renderTextureLut } = await generateIBL(
        cubeMapTexture,
        device,
        format,
        context
      );

      const textureGPU = new TextureGPU({ image: cubeMapTexture, id: id, format: format });
      this.textureMap.set(id, textureGPU)
      return {
        cubeMap: textureGPU,
        ibl: {
          irradianceMap: cubeMapIrradiance,
          prefilterMap: cubeMapPrefilter,
          brdfLUT: renderTextureLut,
        },
      };
    } else {
      return this.textureMap.get(id);
    }
  }
  async loadTexture2DArray(urls, opts = {}) {
    let { id } = opts;
    if (!this.textureMap.has(id)) {
      const texture = new Texture2DArray(opts);
      let images = [];
      let promises = urls
        .filter(url => !this.textureMap.has(url))
        .filter(url => url.constructor.name === "String")
        .map(item => this.requestImage(item));
      let requestData = await Promise.all(promises);
      urls.forEach(async url => {
        if (url.constructor.name !== "String") {
          images.push(url);
        } else if (this.textureMap.has(url)) {
          images.push(this.textureMap.get(url).image);
        } else {
          images.push(requestData.find(item => item.url === url).image);
        }
      });
      texture.image = images;
      this.textureMap.set(id, texture);
    }
  }
  async loadTextureCompressedArrayBuffer(arrayBuffer, opts = {}) {
    let { id } = opts;
    opts.image=arrayBuffer
    if (!this.textureMap.has(id)) {
      const texture = new TextureCompressedArrayBuffer(opts);
      let gpuTexture = texture.toGPU();
      gpuTexture.id=id
      if (opts.isCache) {
        this.textureMap.set(id, gpuTexture);
      }
      return gpuTexture
    }else{
      return this.textureMap.get(id);
    }
  }
  async requestImage(url) {
    if (!this.textureRequestMap.has(url)) {
      this.textureRequestMap.set(url, getImage(url));
    }
    let image = await this.textureRequestMap.get(url);
    this.textureRequestMap.delete(url);
    return { url, image };
  }
  async loadSkyBox(path, device, format, ibl, context, id) {
    if (path.includes("hdr")) {
      const textureCubeMap = await this.loadTextureHDR(path, {
        exposure: 1,
        gamma: 2.2,
        id: id || 1111,
        ibl: ibl,
        device: device,
        format: format,
        context: context,
      });
      return textureCubeMap;
    } else {
      const textureCubeMap = await this.loadTextureCube(path, {
        id: id || 1111,
        ibl: ibl,
        device: device,
        format: format,
        context,
      });
      return textureCubeMap;
    }
  }
  async loadTexture2DType(url, opts = {}) {
    let { id } = opts;
    if (!this.textureMap.has(id)) {
      const texture = new TextureData(opts);
      texture.image = url;
      let gpuTextureObject = await texture.toGPUAsync();
      if (opts.isCache) {
        this.textureMap.set(id, gpuTextureObject);
      }
      return gpuTextureObject
    } else {
      return this.textureMap.get(id);
    }
  }
  clear(){
    this.textureMap .clear();
    this.textureRequestMap.clear();
  }
}
let textureLoader = new TextureLoader();
export { textureLoader, TextureLoader };
